namespace Neovolve.Toolkit.Instrumentation
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Diagnostics;
    using System.IO;
    using Neovolve.Toolkit.Storage;

    /// <summary>
    /// The <see cref="CacheResolver"/>
    ///   class is used to cache a set of 
    ///   <see cref="TraceSource"/> instances.
    /// </summary>
    internal class CacheResolver : ITraceSourceResolver
    {
        /// <summary>
        /// Defines the default cache expiry length for <see cref="TraceSource"/> instances.
        /// </summary>
        public const Int64 TraceSourceCacheExpiryDefaultLength = 60000;

        /// <summary>
        /// Defines the configuration key used to load the value for the <see cref="TraceSourceCacheExpiryLength"/> property.
        /// </summary>
        public const String TraceSourceCacheExpiryLengthKey = "CacheResolver.TraceSourceCacheExpiryLength";

        /// <summary>
        /// Defines the default cache expiry length for trace source names.
        /// </summary>
        public const Int64 TraceSourceNameCacheExpiryDefaultLength = 1200000;

        /// <summary>
        /// Defines  the configuration key used to load the value for the <see cref="TraceSourceNameCacheExpiryLength"/> property.
        /// </summary>
        public const String TraceSourceNameCacheExpiryLengthKey = "CacheResolver.TraceSourceNameCacheExpiryLength";

        /// <summary>
        /// Defines the cache key prefix used to avoid cache key collisions.
        /// </summary>
        private const String TraceSourceCacheKeyPrefix = "CTSM.SourcePrefix";

        /// <summary>
        /// Defines the cache key used to store the list of trace source names.
        /// </summary>
        private const String TraceSourceNamesCacheKeyPrefix = "CTSM.NamesPrefix";

        /// <summary>
        /// Stores the cache used to store <see cref="RuntimeMethodHandle"/> values.
        /// </summary>
        private static readonly ICacheStore _cacheStore = CacheStoreFactory.Create();

        /// <summary>
        /// Initializes a new instance of the <see cref="CacheResolver"/> class.
        /// </summary>
        /// <param name="resolver">
        /// The resolver.
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// The <paramref name="resolver"/> value is <c>null</c>.
        /// </exception>
        public CacheResolver(ITraceSourceResolver resolver)
        {
            // Checks whether the resolver parameter has been supplied
            if (resolver == null)
            {
                const String ResolverParameterName = "resolver";

                throw new ArgumentNullException(ResolverParameterName);
            }

            // Store the resolver
            ChildResolver = resolver;

            IConfigurationStore configStore = ConfigurationStoreFactory.Create();

            TraceSourceCacheExpiryLength = configStore.GetApplicationSetting(TraceSourceCacheExpiryLengthKey, TraceSourceCacheExpiryDefaultLength);
            TraceSourceNameCacheExpiryLength = configStore.GetApplicationSetting(
                TraceSourceNameCacheExpiryLengthKey, TraceSourceNameCacheExpiryDefaultLength);
        }

        /// <summary>
        /// Reloads the <see cref="TraceSource"/> names and instances available from the resolver.
        /// </summary>
        public void Reload()
        {
            String namesCacheKey = GenerateNamesCacheKey();

            // Remove the trace source names from the cache
            _cacheStore.Remove(namesCacheKey);

            List<String> keysToRemove = new List<String>();
            String partialSourceCacheKey = GeneratePartialTraceSourceCacheKey();

            // Loop through each item in the cache
            for (Int32 index = 0; index < _cacheStore.Keys.Count; index++)
            {
                String key = _cacheStore.Keys[index];

                if (String.IsNullOrEmpty(key))
                {
                    continue;
                }

                // Check if this key is for a TraceSource
                if (key.StartsWith(partialSourceCacheKey, StringComparison.Ordinal))
                {
                    // Add this key to the list of keys to remove
                    // Note: We can't remove the item from the cache here because it is in an enumerator
                    keysToRemove.Add(key);
                }
            }

            // Loop through each of the keys to remove
            for (Int32 index = 0; index < keysToRemove.Count; index++)
            {
                String key = keysToRemove[index];

                _cacheStore.Remove(key);
            }

            // Call the stored resolver to reload itself
            ChildResolver.Reload();
        }

        /// <summary>
        /// Resolves a <see cref="TraceSource"/> using the specified name and string comparison.
        /// </summary>
        /// <param name="name">
        /// The name of the <see cref="TraceSource"/>.
        /// </param>
        /// <param name="comparison">
        /// The string comparison to apply.
        /// </param>
        /// <returns>
        /// A <see cref="TraceSource"/> instance or <c>null</c> if the name is not found.
        /// </returns>
        public TraceSource Resolve(String name, StringComparison comparison)
        {
            // Checks whether the name parameter has been supplied
            if (String.IsNullOrEmpty(name))
            {
                throw new ArgumentNullException("name");
            }

            String cacheKey = GenerateTraceSourceCacheKey(name);
            Object cacheObject = _cacheStore[cacheKey];

            // Check if there is a cached object
            if (cacheObject != null)
            {
                TraceSource source = cacheObject as TraceSource;

                Debug.Assert(source != null, "An invalid type has been stored in the cache");

                return source;
            }

            // Resolve the trace source
            TraceSource newSource = ChildResolver.Resolve(name, StringComparison.OrdinalIgnoreCase);

            // Check if there is a trace source available
            if (newSource != null)
            {
                TimeSpan policyLength = TimeSpan.FromMilliseconds(TraceSourceCacheExpiryLength);
                RelativeExpirationPolicy expirationPolicy = new RelativeExpirationPolicy(policyLength);

                // Add the item to the cache
                _cacheStore.Add(cacheKey, newSource, expirationPolicy);

                // Return the item
                return newSource;
            }

            // No trace source was found
            return null;
        }

        /// <summary>
        /// Gets the trace source names.
        /// </summary>
        /// <returns>
        /// A <see cref="Collection{T}"/> instance.
        /// </returns>
        public Collection<String> ResolveNames()
        {
            String namesCacheKey = GenerateNamesCacheKey();

            // Check if the names are in the cache
            if (_cacheStore[namesCacheKey] != null)
            {
                // Return the cache value
                return _cacheStore[namesCacheKey] as Collection<String>;
            }

            // Get the names from the resolver
            Collection<String> names = ChildResolver.ResolveNames();

            if (names != null)
            {
                // Store the names in the cache
                _cacheStore[namesCacheKey] = names;
            }

            return names;
        }

        /// <summary>
        /// Generates the names cache key.
        /// </summary>
        /// <returns>
        /// A <see cref="String"/> value.
        /// </returns>
        private String GenerateNamesCacheKey()
        {
            // The cache key for the trace source names is the combination of the key prefix and the resolver type name
            return String.Concat(TraceSourceNamesCacheKeyPrefix, Path.PathSeparator, ChildResolver.GetType().AssemblyQualifiedName);
        }

        /// <summary>
        /// Generates the partial cache key for trace sources.
        /// </summary>
        /// <returns>
        /// A <see cref="String"/> value.
        /// </returns>
        private String GeneratePartialTraceSourceCacheKey()
        {
            // The cache key for this trace source name is the combination of the key prefix, the resolver type name and the trace source name
            return String.Concat(TraceSourceCacheKeyPrefix, Path.PathSeparator, ChildResolver.GetType().AssemblyQualifiedName);
        }

        /// <summary>
        /// Generates the cache key for a trace source name.
        /// </summary>
        /// <param name="traceSourceName">
        /// Name of the trace source.
        /// </param>
        /// <returns>
        /// A <see cref="String"/> value.
        /// </returns>
        private String GenerateTraceSourceCacheKey(String traceSourceName)
        {
            // Make the name uppercase in order to cover cases where the same name is used with different casing
            String name = traceSourceName.ToUpperInvariant();

            // Get the initial part of the key
            String partialKey = GeneratePartialTraceSourceCacheKey();

            // The cache key for this trace source name is the combination of the key prefix, the resolver type name and the trace source name
            return String.Concat(partialKey, Path.PathSeparator, name);
        }

        /// <summary>
        /// Gets or sets the length of the trace source cache expiry.
        /// </summary>
        /// <value>
        /// The length of the trace source cache expiry.
        /// </value>
        public static Int64 TraceSourceCacheExpiryLength
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets the length of the trace source name cache expiry.
        /// </summary>
        /// <value>
        /// The length of the trace source name cache expiry.
        /// </value>
        public static Int64 TraceSourceNameCacheExpiryLength
        {
            get;
            set;
        }

        /// <summary>
        /// Gets the child resolver.
        /// </summary>
        /// <value>
        /// The child resolver or <c>null</c> if there is no child resolver.
        /// </value>
        public ITraceSourceResolver ChildResolver
        {
            get;
            private set;
        }
    }
}